python 例外処理について考える
#python #例外処理 #zenn投稿予定
ポイント
例外処理の話では「EAFP vs LBYL論争」があるが、pythonではEAFPの方針が採用されることが多い(可読性の観点から)
pythonはtry-catchのオーバーヘッドが小さいので、そういう面からもEAFPを使って良い。
契約の観点から、異常は「契約違反」と「例外」の2つに分けれる。
回復の観点から、異常は「回復可能な異常」と「回復不可能な異常」の2つに分けれる。
そのシステム用のエラーを実装しておくと良さげかも?
エラーのログを記録する場合はスタックトレースも記録すること。エラーメッセージだけの記載はバグを追いにくくする。
例外は本来起こってはいけないものという意識で、例外処理が最低限になるよう努めること
こちらがどうなるか想定できない部分は例外として処理できるようにしておくといい(I/Oなど)
tyr-catchは「例外が発生してもエラーが出ないようにするための仕組み」ではない!
原則として回復不可能な例外は処理の一番親部分で例外処理する。
子ルーチンで発生した回復不可能な例外は、途中でわざわざcatchせずに親まで回してあげればいい
そうすれば親が一緒に回復不可能な処理として共通に扱ってくれる
基本的には、スタックトレースをログに流すとかになるだろう。
回復可能な異常のみcatchして例外処理をすること
例外処理を担当する層はどこでもいいが、なるべく早く処理してあげた方がいい
回復可能な以上のみを「例外」と定義する
回復不可能な異常は全て最上層(親)で処理する
自作例外クラスは基本的に必要になってくる。回復処理の際にどんな例外が起きたのかを知る必要があるから。
契約違反を例外として扱うか否かは、言語の仕様や支持する論説によって変わりそう...
例外処理をしっかり行うことでバグが見つけやすい堅牢なアプリを作ろう
例外をフローコントロールに利用しない
契約範囲外の処理を例外として扱う。それ以外に例外処理(try-catch)を使ってはいけない。
内部実装に依存した例外クラスを返さないこと
ルーチン内部の実装に依存した例外ではなく、抽象化した例外を返してあげると良き。
難点
q.icon 回復可能な異常が起きた際、異常が起きたルーチンが処理すべきなのか、それとも呼び出し先の誰か(呼び出し先の先の...)が処理すべきなのか...わからない
a.icon 呼び出し先の誰か
q.icon ライブラリのような別システムから呼び出されるようなシステムじゃないなら、そのシステム用のルートExceptionを定義する必要はない?
a.icon 必要はないっちゃないが、設定することによるデメリットもないので、どっちでもいい
q.icon 契約違反は例外として処理すべき?それともチェック関数を作ってそこで違反チェックを行うべき?
a.icon 例外処理として扱った方がいい
というか違反チェックメソッドを作ったとしても、呼び出し元がそれを使ってくれるとは限らないので、やはり例外にしたほうがいいかも
q.icon ちょっと複数の参考記事の中で「例外」の定義が混乱してるな...
a.icon 「~~Exception」的なのは全部「例外」って覚えとけばOK
q.icon 回復不可能な例外は全て最上位のルーチンに回すと言うけれど、ロールバックのような事象が必要な場合は、回復不可能だろうがなんだろうが、例外をキャッチせなあかんよな。
ここの考え方ってどう指針に組み込んで行こうかなonigiri.w2.icon
a.icon (指針への組み込み方は知らんけど)
例外が発生した箇所から遡った、どこかの親ルーチンでロールバック処理を実施するしかない
注意.icon 複数のDBを一回のユースケースで弄る時は、タイミングを同じルーチンで扱ったほうがいいように思う
そのルーチンでロールバック処理できるようにしておくといいかと
参考
Pythonで例外を投げるときのベストプラクティス - Qiita
66:専用の例外クラスでエラー原因を明示する — 自走プログラマー【抜粋版】
Pythonの例外処理はコードの可読性を上げる便利な機能
python errorハンドリングのベストプラクティスがわからない
Pythonの例外処理に関するまとめ - minus9d's diary
例外設計における大罪
例外設計の話
第12話 例外は例外だから例外じゃないの?:ソフトウェア開発に幸せな未来はあるのか:エンジニアライフ
<Python>悪い例外処理とraiseへの誤解 - のーずいだんぷ
エラーログとtry-catchの最低限 - プログラマのはしくれダイアリー
契約による設計から見た例外 - Qiita
アプリケーションと三種の異常 | DevelopersIO
例外処理とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
プログラミングにおける例外処理の考え方、設計指針 - applis
ログ
hr.icon
<Python>悪い例外処理とraiseへの誤解 - のーずいだんぷ
pythonにおける例外処理のプラクティス
自作例外で例外を処理する
アンチパターン
if else(goto)的な使い方をしてはいけない
例外が発生した後に通常制御に戻すようなことをしてはいけない
例外が発生した時点でログなどの記録をして処理を終了すること
例外が発生した後に通常の処理の戻そうとすると、思わぬ弊害を受けることになる
以下のようなif else的な使い方はなしonigiri.w2.icon
code: example.py
def div_ten(x):
try:
if x==0:
raise ValueError
return 10//x
except ValueError:
# また全く別の計算
エラーログとtry-catchの最低限 - プログラマのはしくれダイアリー
例外処理とコード量のジレンマ
一方で、例外処理のコード数が減るという点でコード全体行数は少なくて済む。
ただし、例外処理を減らす(広くすればするほど)エラーの抽象度が上がるというジレンマがありそう。
例外発生時に、エラー原因の特定を容易にするためには、できる限りtry-catchのブロックを狭くする必要があるonigiri.w2.icon
だが...、try-catchのブロックを狭くして、より多くの箇所にtry-catchの文を入れてしまうと可読性が落ちる。
このジレンマをどうしようって話
try-catchを成立させるにはstacktraceが不可欠
try-catchはそのように曖昧なのに、様々な言語で採用されているのは
(普段当たり前すぎて考えもしていなかったけど)スタックトレースがあるからだと思う。
それにより曖昧なエラー発生を明確に追跡可能にしている。
つまりスタックトレースのないtry-catch=死。個人的には。
エラー発生時にスタックトレースが見えないと死ぬonigiri.w2.icon
そんな処理方法にできてるか?ちょっと確認するか。
メッセージはgrepbilityを考える
メッセージの検索性を強くしておく
スタックトレースは必須
第12話 例外は例外だから例外じゃないの?:ソフトウェア開発に幸せな未来はあるのか:エンジニアライフ
例外は本来、起こってはいけないこと(のはず)だから、きちんと設計して例外が出ないようにすべきではないのか?
確かに前提はこの心構えでいた方が良さそうonigiri.w2.icon
ただ、例外を全て無くすことはできないと思う。
利用してるDBが突然なくなったら、DBに接続できなくなってエラーが発生する。
接続不可が起きた瞬間、通常制御が効かなくなるので例外フローとして扱うことになる。
基本方針は、やはり例外がなるべく出ないような設計、コーディングすべきということです(しつこいですか?)。
ただし、I/Oやデータベースアクセスなどは相手がどうなるかまでこちらで関知できないので、そういう部分はちゃんと例外として処理させるべきでしょう。
ここ重要で、相手がどうなるか予測できない。確実な動きをしてくれるとは限らない処理の場合は例外処理を書かないとダメonigiri.w2.icon
ただ、わたしが感じるのは、「try~catch」を「例外が発生してもエラーを出させないための機構」と勘違いしているエンジニアが意外と多い、ということです。
エラーが起きたら、もう終わりなんだよonigiri.w2.icon
通常フローには戻れないんだよ。覚えときな。例外なんだから。
アンチパターン
1. 例外握りつぶし(ASP.NET/VC#)
2. 不必要なthrows(Java)
よくあるアンチパターンですねonigiri.w2.icon
「エラーを正しく処理すること」と「例外が出ないようなコーディングを考えること」が一番だと思っています。
流れを止めてでも「エラー」にすべき時はちゃんと理由を明確にしてお客様と交渉すべきです。エラーをリカバリできるシステムなら問題ないですが(そういうシステムは作ったことがあるので)、そうでなければ確実に欠陥品ができあがります。
例外設計の話重要.icon
このページはとても役に立ちますonigiri.w2.icon
回復可能な例外だけをキャッチする必要があります。たとえば、存在しないファイルを開こうとした場合に発生する FileNotFoundException は、アプリケーションで処理できる例外です。それは、アプリケーションがユーザーに問題を知らせ、ユーザーが別のファイル名を指定したり、ファイルを作成したりできるようにすることが可能だからです。ExecutionEngineException を生成するような、ファイルのオープン要求は、例外の根本原因が把握できず、実行を継続することの安全性をアプリケーションが保証できないため、処理しないでください。
回復可能なら、継続を促す処理を記載するのも可能なのかonigiri.w2.icon
エラー出たからはい終わり。最初からやり直してください。ではないのね。
ただ回復可能なエラーって、そもそも例外処理の前に防げたりしないのか?
ユーザーが間違えた入力を記載するときとかは....バリデーションかけて制御するとかできる
エラー処理にしなくても良さそうな気がする。
valid() -> bool処理でどうにかすればいいんじゃね
例外と返り値の違い
例外は投げっぱなしにできる。どうせリカバリの効かない異常の場合には、発生直後でストップしそのまま正常系のコードとしてはその処理を無視したまま、コードの可読性を損なわずに書くことができる。返り値だと、リカバリ不能なケースでも明示的に上位層へ受け渡しをしていかないとどこで問題が発生したかの情報が途切れてしまう (もしくはそのためにアプリケーションの深いところでのロギングが必要となる)。
可読性向上。正常処理には戻さない。戻すと可読性が終わる。
例外処理の指針
・「リカバリ可能」な「異常な状態」の場合に、アプリケーション例外を使用する
・例外の catch は「リカバリ」を担当すべき層が早めに拾ってその中で閉じて処理をする
・それ以外の例外については投げっぱなしにして最上層で復帰できない例外として処理する
・SystemError を catch してハンドルしようとしない (上位で最後に拾わせる)
・リカバリ可能な例外 (意図的に上の層で catch し対処が決まっている例外) については、そのケースだけを catch できるようにアプリケーション例外のクラスを定義する
・検査例外的な発想で、明示的に上位層へそのアプリケーション例外が投げられることを知っていてほしいケースというのを無くす。アプリケーション例外は局所的に閉じて使い、その影響を漏らさない。これで、上位層では RuntimeError が起きていれば、復帰不可能なエラーだという判断して、ロギングして処理を停止した通知のみをクライアントへ返すことができる。
・投げっぱなしでいいって言ってるんだから、わざわざ握りつぶさないこと
・リカバリするか投げっぱなしにするかと言ってるので、例外の読み替えはほとんど必要ない。自分で処理しないならそのまま投げる
・経路をたどるのはスタックトレースに任せる。例外発生時の経路をたどれるように例外 (を catch したところでのロギングや例外の読み替えによる情報追加) でサポートしようとするとどんどん処理が深くなり、必要な情報が埋もれる。
クラスをつくるの? RuntimeException のメッセージにエラータイプを書けばいいの? よくある悩みだが、これもすぐ上の層で catch してハンドルするときの利用のされ方を考えればいい。エラーの内容によって処理がわかれるならクラスとしてわける。処理はいっしょで内容が伝わればいいだけなら、メッセージに書いておけば ok。
ログイン失敗は例外?
ログインでパスワード不一致なのは、当然考えられるべきケースなので、例外ではなく認証の失敗時のフローとして扱われるべき。
業務的に想定される失敗?のようなものは、例外として扱うのはなく普通の処理のように扱えって感じか。
さっきの記事にも出てきた原則に当てはまりそう
-> 「例外は本来、起こってはいけないこと(のはず)だから、きちんと設計して例外が出ないようにすべきではないのか?」
その例外発生から回復できないなら、一番上位の例外処理に任せちゃいな!!!onigiri.w2.icon
きっとログを記録してエラー処理として適当に終わらせてくれるよ
例えば「500 Error」とか出してな
例外設計における大罪
「例外は呼び出す側が契約条件を満たしたが呼び出された側が契約を履行できなかった時に投げるもの」
これはめちゃくちゃスッキリする考え方onigiri.w2.icon
さっきのログイン認証失敗の話は、適切なパスワードを入れろよ!!っていう契約なのに、向こうが違反してるから例外ではない。ただの違反。
こういう時は例外処理をせずに、通常のフローとして処理してあげるべき。
回復可能なエラー以外は、上層に処理をまるまかせしちゃいな!!onigiri.w2.icon
Pythonの例外処理に関するまとめ - minus9d's diary
Pythonでは「認可をとるより許しを請う方が容易 (easier to ask for forgiveness than permission)」、略してEAFPというコーディングスタイルが推奨されています。EAFPは、エラーを起こすかもしれない処理もまず実行してみて、もしエラーが出たらそのとき後始末をする、というスタイル
何言ってるのかわかんない。後で噛み砕いてみるかonigiri.w2.icon
Python: EAFP スタイルと LBYL スタイル - CUBE SUGAR CONTAINER
なるほど、EAFPスタイルは一般的な原則に反してるような?
原則「例外は例外として扱うべきであり、if else、gotoのような使い方をするな」
そもそも「0」が入れられたら、回復すべきなのか?
回復すべきなら、このエラー処理は回復処理ってことになるのかな
if else的な使い方ってわけでもないか。難しい。
Exceptionクラスの説明には「システム終了以外の全ての組み込み例外はこのクラスから派生しています。全てのユーザ定義例外もこのクラスから派生させるべきです」とあります。
なるほど、Exceptionクラス以外の派生のエラーがあるのねonigiri.w2.icon
ptyhonはこいつがルートだと思ってたが違うらしい。Errorとかがrootかな
スタックトレースを表示重要.icon
例外処理をするとスタックトレースが表示されなくなってしまうので、デバッグのときに不便です。例えば以下のコードを実行すると、division by zeroとしか表示されず、エラー箇所がわかりません。
code: sample.py
def sample():
try:
1 / 0
except Exception as e:
print(e)
-> division by zero
スタックトレースを表示するにはtracebackを利用する
code: sample.py
import traceback
def sample():
try:
1 / 0
except Exception as e:
traceback.print_exc()
code: resutl
Traceback (most recent call last):
File "error_test.py", line 26, in <module>
func2()
File "error_test.py", line 23, in func2
return func1()
File "error_test.py", line 20, in func1
1 / 0
ZeroDivisionError: division by zero
Pythonの例外処理はコードの可読性を上げる便利な機能
システムがクラッシュした結果が例外だというイメージを持つために、なるべくif文を書いて例外処理を書こうとしないことがある。
でも、それは間違っている。
例外はよく起こることで、例外と親しくして例外を受け入れる、積極的にスローすることが大切だ。
なるほど?ある人の言う原則とは違うのか。pythonだけか?これは。
例外が起きたらシステムは終わりだ!なんて考える必要はなく、例外を当然のこととして処理しよう。
例外を起こす(raise)することはとてもPythonという言語の理にかなっていて、例外処理を書くのを避けようとすることこそ避けるべきです。
ん〜〜〜〜。色んな見方があるなぁ。どうやって折り合いつけよかなonigiri.w2.icon
全部を全部例外処理にはしたくないんよなぁ...
確かにpythonのtry-catchをうまく使えば、可読性を逆にあげることにもつながる。
くぅ
ゼロで割りたくないんだから、if文でゼロでないか確かめてから割ればええやん!っていう考えです。
でも、ちょっと待って下さい。
そのif文の処理ってプログラムの本質的なところではない、言い換えれば本当にやりたいことじゃないのにのさばっているような感覚を受けませんか?
言ってることはとてもわかるonigiri.w2.icon
契約違反の入力値にわざわざif文使うのは嫌っちゃ嫌。
てなるとValidationErrorとか使って、契約違反を取り締まった方がいいかな
てか契約違反系は全部ValidationErrorを出してさ。全部上層に与えれば...
ん〜〜〜それも違う気がするなぁ
そもそもここの話はあれだ、LBYL vs EAFP論争のやつだonigiri.w2.icon
どっちが正しいのかよくわからん
いや、ちょっと違うかも
66:専用の例外クラスでエラー原因を明示する — 自走プログラマー【抜粋版】
正常系と異常系の処理の見分けがつかない実装コードになっているため、コードを読み解くのが難しくなっています。
異常の時は例外を出せって感じonigiri.w2.icon
異常には2つある。
契約違反
契約遵守してるのに失敗(例外)
専用の例外クラスを自作して、エラーを明示的に実装しましょう。
発生するエラーの種類ごとに専用の例外クラスを定義して、それぞれ異なるエラーメッセージを表示するように実装します。 また、各例外の親クラスを定義しておけば、例外処理を行うコードで同系統の例外をまとめて捕まえられるため、簡潔でわかりやすい実装になります。 前述のコード用に例外クラスを実装すると、以下のようになります。
code:: exceptions.py
class MailReceivingError(Exception):
pretext = ''
def __init__(self, message, *args):
if self.pretext:
message = f"{self.pretext}: {message}"
super().__init__(message, *args)
class MailConnectionError(MailReceivingError):
pretext = '接続エラー'
class MailAuthError(MailReceivingError):
pretext = '認証エラー'
class MailHeaderError(MailReceivingError):
pretext = 'メールヘッダーエラー'
この実装いいね!!onigiri.w2.icon
アプリ専用のエラーを作ってハンドリングするタイミングを具体的にイメージできてないけど。
ただ、使ってる時にここや!!みたいなタイミングはありそう。
Pythonで例外を投げるときのベストプラクティス - Qiita重要.icon
ライブラリ開発やデータ分析ツールの作成の際に適切に例外処理を行うことで、頑健かつバグの発見をしやすいシステムを作れるようになる。Pythonは他言語と比べて例外処理のオーバーヘッドが軽いので積極的に利用することで、高速かつ安全なコードを書くことができます。
バグが見つけやすいコードを作る!!!!onigiri.w2.icon
AWSが発するエラーメッセージのように、何がエラーなんか具体的にわからんのは厳しい。
投げる例外は適切に文書化する
最も重要なことです。定義した例外がどういうものなのかを適切にドキュメント化しましょう。毎度毎度書くのは面倒なので、名前だけで伝わるような命名にする、__str__ に詳細な説明を書く、もしくはSphinxなどのドキュメント自動生成ツールを使うのがオススメです。
ホイホイ。名前に意味のない例外だけは作らないですonigiri.w2.icon
ライブラリ共通の例外を作成し、全てそれを継承させる
これをすることで、そのライブラリが投げた全ての例外を簡単に受け取ることができます。
ライブラリ作成時は確かに必要なことやと思うonigiri.w2.icon
ただ、Webアプリとか作ってる際は、必ず必要か?と言われるとどうやろ...って感じ。
別にライブラリ的な使い方をされることはないので、Exceptionをそのまま使ってもいいんじゃね?って思ったりする。
Webアプリの全ての例外に共通で持たせたい処理があるなら作ってもいいかな
LBYLよりはEAFPを採用しよう
LBYL版では新しいオブジェクトを作る度にif文を追加する羽目になるのでバグを生みかねません。一方、EAFP版では対象のobjectに__str__が実装されていれば実行できるので、コードも短く可読性が増していることが分かります。
code: sample.py
def print_object_lbyl(some_object):
# Check if the object is printable...
if isinstance(some_object, str):
print(some_object)
elif isinstance(some_object, dict):
print(some_object)
elif isinstance(some_object, list):
print(some_object)
# 97 elifs later...
else:
print("unprintable object")
def print_object_eafp(some_object):
# Check if the object is printable...
try:
printable = str(some_object)
except TypeError:
print("unprintable object")
else:
print(printable)
フローコントロールに利用するな
Goto 的な感覚で利用するのは禁忌です。例えば、リストから該当する文字列を検索するプログラムを作る際、見つからなかった場合に例外を投げるのは誤りです。Indexを超えた場合など、起こるべきではないケースで例外を返しましょう。
code: sample.py
def string_finder_wrong(l: Sequencestr, s: str, end: int) -> str:
result = None
for i in range(end):
if (li == s):
result = s
if result is None:
raise NotFoundError # 見つからなかったときに例外処理でコントロールするのは良くない
def string_finder_good(l: Sequencestr, s: str, end: int) -> Optionalstr:
if (len(l) <= end):
raise IndexError # OutOfBoundsなので例外を投げる
result = None
for i in range(end):
if (li == s):
result = s
return result # 該当しなかった場合はNoneを返す
契約内容通りの処理なら例外を返すなってことかと思われるonigiri.w2.icon
利用する側もびっくりしちゃうよ
内部実装が漏れる例外重要.icon
カプセル化が保たれるように例外を設計しましょう。例えば、URLからHTMLを返すような実装をするときに、キャッシュを使いたいとします。その際に、キャッシュのファイルが開けないときに IOError を投げるのは誤りです。というのも、内部実装は変更されることがあり、今後はS3にキャッシュを保存し、それをAthenaでアクセスしたくなるかもしれません。内部実装によって例外が変わる場合、実装を変えるたびこの機能のユーザーは例外処理を書きなおさなければならなくなります。
code: sample.py
def url2html_wrong(url: str) -> str:
try:
with open(CACH_FILE):
# ファイルの中身を操作する何かしらの操作
except IOError:
raise IOError # 内部でファイルIOを利用していることがバレる。
def url2html_good(url: str) -> str:
try:
with open(CACH_FILE):
# ファイルの中身を操作する何かしらの操作
except IOError:
raise CashNotFoundError # キャッシュ周りでエラー吐いたことが伝わる
これめっちゃ「ハッ」とした!onigiri.w2.icon
確かに、言われてみればそうやわ。
内部実装が変わったら、エラー名も変わるとかめっちゃしんどい。
変更容易性が全くの皆無。
できる限り実装に依存したエラーを返さない。
プログラミングにおける例外処理の考え方、設計指針 - applis
例外処理とは、呼び出し先に想定していないことが起きたときに、問題の解決=回復を行うことをいいます。
回復できないものは例外処理と呼ばずなんと呼ぶの?onigiri.w2.icon
例外の定義が難しいねんなぁ...
回復できないのは無視くらいの態度でいいと思うねんなワシは
例えばDB接続不良は、回復できないよ。ユーザーにはどうすることもできない。
まあ、ユーザーが誰か次第やけど。
それでは、実際にどうやって例外処理を行えばいいのでしょうか。基本的な考え方は、『例外処理は回復するか投げっぱなしにするかのどちらか』で対応するとよいです。
これは自分の今考えてる方針と合致してるなonigiri.w2.icon
例外処理は、呼び出し先で起こりうる例外を回復できるところで行います。
呼び出し先で起こりうる例外を回復できないところでは例外処理をすべきではありません。
例外が起こりうる呼び出し先があるからといって、直近の呼び出し元で例外処理を必ずしなければならないという訳ではありません。上位の呼び出し元に任せることも、大切な考え方のひとつといえます。
ここも、現状の自分の例外処理方針と合致してそうonigiri.w2.icon
呼び出し元のどれかが例外処理をすればよくて、例外発生させたルーチンを直で呼び出したルーチン(直の呼び出し元)が例外処理を必ずするわけではない
例外はいつ投げるべきなのか?
『呼び出しもとは呼び出す条件を満たしたけど、求められる処理を達成できなかったとき』に投げればよいです。
ここは自分の考え方と少し違うonigiri.w2.icon
条件を満たした(契約遵守)のに契約通りの処理を遂行できなかった時に例外を投げるのは確かにそう
ただもう一つ例外を投げる場面がある
それは呼び出し元が呼び出し条件を満たさなかった時
この時も例外を投げる必要がある。そのままだと処理を遂行できない。
ただ、ここはEAFPvsLBYLの話が関わってくるかも
EAFPの考え方なら「契約遵守されなかったら例外を出す」感じでいい。
LBYLの考え方なら「契約遵守してるかどうかを事前にメソッドとかでチェックしておけ」てなる。
・呼び出し元に例外処理を強要しないこと
・例外クラスを定義するということは、呼び出し元で回復処理を期待することを意味する。そうでなければ標準例外クラスを使い、クラス定義はしない
確かにそうやんな。その例外をキャッチして処理するから例外定義するんよな
ここはしっかり覚えておきたいところ
所感
Webサービスの観点から異常を捉えたとき
回復可能な異常:ユーザーが解決できる問題
q.iconユーザーが関わらない途中の呼び出し元が回復できる異常って例外にすべきなの?
ZeroDivisionって途中で回復できる?できんよな。ユーザーに値を訂正してくれって言わないとあかんよな。
回復不可能な異常:これはシステム管理者が解決しないといけない問題
論点.icon 契約違反のチェックって連鎖しない??onigiri.w2.icon
子ルーチンの契約を守るために、親からの呼び出し時に子ルーチンの契約も含めて契約チェックするよな?
これって多重チェックになっちゃう気がするんだが...
こうなると子ルーチンの契約チェックっていらんくなってくるよな...
全部親ルーチンで契約チェックを済ませて仕舞えば、それ以降の子孫ルーチンではチェックしなくてよくね?
ただ、子ルーチンが別の親ルーチンから呼び出されてる時は?
その時も子ルーチンを使うやつがチェックすればよくね?
抜け漏れがないようにする
多重防御っていう考え方があるやん?
防御しすぎて問題ってなくない?
-> 性能にわんちゃん影響するのよ
ここは難しいところね。論点としておいておこう。
契約による設計から見た例外 - Qiita重要.icon
この記事はめちゃくちゃいいこと書いてるんやけど要約できねぇ...onigiri.w2.icon
onigiri.w2.iconの「契約」に対する見逃し
事前条件が満たせてないことで起きるエラーは全て呼び出し元の責任なのか?
答えはNo
呼び出される側が呼び出し側に事前条件を満たせてるかの手段を提供してる時のみ「Yes」
呼び出し側が事前条件を満たすためには、呼び出し元の現状をチェックする必要があるかも知らん
その現状を知る手段がないのなら、事前条件を満たすこともできない。
これは不公平な状態。
事後条件が満たせてない場合は、呼び出し元の責任であることは明白
呼び出し元は事前条件を満たしたのにも関わらず、呼び出し先が失敗してる。
不変条件が満たせてない場合は、誰の責任?
呼び出し元がカプセル化の状態を守っているという前提では、呼び出し先の原因になる。
カプセル化が守られてないと、責任探しをするのは面倒臭いことになる可能性あり。
わけわからんやつが、呼び出し先の値を勝手に変更してたりすることも考えられるから。
「契約」から見た例外回復とは、契約通りの事後条件・不変条件を満たせるようになるってこと
回復をするのはルーチンチェーンの中の誰でもいい。
子ルーチンで例外が発生して、その例外が親世代に派生したとしても、親ルーチンの誰かが例外をキャッチして、親ルーチンが達成すべき事後条件を満たしてくれるなら、万事OK.
これが例外の回復。
まあ、最終的にはエンドユーザーまで回るかもしれんけど。
でもそれも契約のあーだこーだに当てはめて考えれそうであるんやけどな。
ユーザーが契約を満たしてなかったから、正しい方法でやり直す的な。
ん?俺何言ってる?でも言いたいことはわかるような
「契約」の観点からの例外を見てきたけど、異常が起きた時の責任の所在がないものが1つだけ存在する。
それが、外部APIなどを叩く時などの信頼性が100%じゃない呼び出し先を利用して起こるエラー。
たとえばDBアクセス、外部API呼び出しとか、そういう時に発生する接続エラーなどは、責任の所在がコード内に存在しない。
こういうのは、全てコード上ではもう対処のしようがないバグになる。
前日に述べたように、例外は回復するか通知するかのどちらかです。
これは金言onigiri.w2.icon
でもそう。回復可能な異常、回復不可能な異常の2つしかない。
回復不可能な異常は管理者・開発者に通知するしかない。
回復可能な異常は、ルーチンのどこかのタイミング(エンドユーザーの可能性もある)で回復処理を行う。
ある処理の契約違反を別の処理が尻拭いしてくれれば、それで良いというわけです
これも重要。契約違反とかどうかはどうでもよくて、契約違反が起きたときに例外を回復できるのかできないのか、ただそれだけ。
開発者がコード作成時に契約違反を起こすような処理を書いてしまってるか、システム稼働時にユーザーが契約違反になるような行動、入力を行ってるのか
開発者起因の契約違反は単体テストで抹殺してあげないとダメ
onigiri.w2.icon的ルール
1.icon 各ルーチン(メソッド、関数)には契約を定義しておき、契約違反(事前条件の未達成)は全て例外を発生させる。
2.icon 契約遵守したのに発生する異常の中でも「呼び出し元の行動次第では回復可能なもの」は例外を発生させる
3.icon 契約遵守したのに発生する異常の中でも「どうにもならない、回復が見込めないもの」は最上位層のルーチンでキャッチして処理する
異常元のルーチンは例外すら出さない。勝手に出る例外をそのまま最上位ルーチンまで回す。
回復の見込みがない例外をわざわざルーチンの途中でキャッチしないこと。
全て上位層に回しなさい。
また、回復の見込みがない例外に自作の名前をつけないこと。
「自作の例外クラスで例外を出し直す」的なことはしなくていい。
code: ng.py
from mysqldb import db, DBException
class SampleIOException(Exception):
pass
def raiser():
try:
client = db.connect()
except DBException:
# こんなことしなくていい。回復見込みがどうせないのだから、いらんことしない。
# スタックトレースが汚れて、読みにくくなる。
raise SampleIOException
これは所謂「バグ(Bug)」である。
開発者がコード変更、もしくはシステムのどこかを弄らない限りは治らないバグである。
4.icon ルーチンで例外を発生させる場合は、ライブラリ・システムのエラーをそのまま出さずに、分かりやすい名前の例外でラップして発生させること
例外を発生させるときは、どこかのルーチンで回復処理が入れられるわけなのだが、具体実装の名前がついた例外をキャッチさせてしまってはだめ。
例外発生ルーチンでは、抽象名のついた例外でラップして例外発生させること。
「抽象に依存させなさい」
これは「例外処理」の世界でも例外ではありません。
実際にコードを書いてみての感想
「異常」は以下のカテゴリに分けれる
事前条件違反による異常
回復処理可能な異常(エンドユーザ〜異常発火の親ルーチンまでの間で)
事前条件遵守してるけど異常
回復処理可能な異常(エンドユーザ〜異常発火の親ルーチンまでの間で)
回復不可能な異常
事前条件違反はユーザー起因のはずなので、最終的に異常が起きたらユーザーまで知らせてあげる
q.icon「それ以外の起因もあるのでは?」に対する回答
a.icon 単体テストをちゃんとやっておけば無さそう
と今は考えてるが、後々考えが変わるかも...
事前条件のチェックはassertを使えばいい。コードの可読性を保てる。
ていうか...ユーザーIFの最も近い場所で入力値チェック行えばいいのでは?
多重防御か境界線防御かの論争になるぅ〜〜〜〜
究極キャッチできない異常に関しては、スタックトレースを残しておけば最悪なんとかなる。